package my.mvc;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;

/**
 * Created by hackway on 2016/5/16.
 */
public class ActionServlet extends HttpServlet {

    private final static Logger log = LoggerFactory.getLogger(ActionServlet.class);
    public final static byte METHOD_GET = 0X01;
    public final static byte METHOD_POST = 0x02;

    public final static String ACTION_FORWARD = "__action_forward__";
    private final static String ERROR_PAGE = "error_page";
    private final static String UTF_8 = "utf-8";
    private List<String> action_packages = null;
    private final static HashMap<String, Object> actions = new HashMap<String, Object>();
    private final static HashMap<String, Method> methods = new HashMap<String, Method>();


    @Override
    public void init() throws ServletException {
        //loadAnnotationMethod();  //加载注解的action类
        String tmp = getInitParameter("packages");
        action_packages = Arrays.asList(StringUtils.split(tmp, ','));
        String initial_actions = getInitParameter("initial_actions");
        for (String action : StringUtils.split(initial_actions, ',')) {
            try {
                _loadAction(action);
            } catch (Exception e) {
                log.error("Failed to initial action : " + action, e);
            }
        }
    }

    /**
     * Called by the server (via the <code>service</code> method) to
     * allow a servlet to handle a GET request.
     * <p/>
     * <p>Overriding this method to support a GET request also
     * automatically supports an HTTP HEAD request. A HEAD
     * request is a GET request that returns no body in the
     * response, only the request header fields.
     * <p/>
     * <p>When overriding this method, read the request data,
     * write the response headers, get the response's writer or
     * output stream object, and finally, write the response data.
     * It's best to include content type and encoding. When using
     * a <code>PrintWriter</code> object to return the response,
     * set the content type before accessing the
     * <code>PrintWriter</code> object.
     * <p/>
     * <p>The servlet container must write the headers before
     * committing the response, because in HTTP the headers must be sent
     * before the response body.
     * <p/>
     * <p>Where possible, set the Content-Length header (with the
     * {@link ServletResponse#setContentLength} method),
     * to allow the servlet container to use a persistent connection
     * to return its response to the client, improving performance.
     * The content length is automatically set if the entire response fits
     * inside the response buffer.
     * <p/>
     * <p>When using HTTP 1.1 chunked encoding (which means that the response
     * has a Transfer-Encoding header), do not set the Content-Length header.
     * <p/>
     * <p>The GET method should be safe, that is, without
     * any side effects for which users are held responsible.
     * For example, most form queries have no side effects.
     * If a client request is intended to change stored data,
     * the request should use some other HTTP method.
     * <p/>
     * <p>The GET method should also be idempotent, meaning
     * that it can be safely repeated. Sometimes making a
     * method safe also makes it idempotent. For example,
     * repeating queries is both safe and idempotent, but
     * buying a product online or modifying data is neither
     * safe nor idempotent.
     * <p/>
     * <p>If the request is incorrectly formatted, <code>doGet</code>
     * returns an HTTP "Bad Request" message.
     *
     * @param req  an {@link HttpServletRequest} object that
     *             contains the request the client has made
     *             of the servlet
     * @param resp an {@link HttpServletResponse} object that
     *             contains the response the servlet sends
     *             to the client
     * @throws IOException      if an input or output error is
     *                          detected when the servlet handles
     *                          the GET request
     * @throws ServletException if the request for the GET
     *                          could not be handled
     * @see ServletResponse#setContentType
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        process(RequestContext.get(), METHOD_GET);
    }

    /**
     * Called by the server (via the <code>service</code> method)
     * to allow a servlet to handle a POST request.
     * <p/>
     * The HTTP POST method allows the client to send
     * data of unlimited length to the Web server a single time
     * and is useful when posting information such as
     * credit card numbers.
     * <p/>
     * <p>When overriding this method, read the request data,
     * write the response headers, get the response's writer or output
     * stream object, and finally, write the response data. It's best
     * to include content type and encoding. When using a
     * <code>PrintWriter</code> object to return the response, set the
     * content type before accessing the <code>PrintWriter</code> object.
     * <p/>
     * <p>The servlet container must write the headers before committing the
     * response, because in HTTP the headers must be sent before the
     * response body.
     * <p/>
     * <p>Where possible, set the Content-Length header (with the
     * {@link ServletResponse#setContentLength} method),
     * to allow the servlet container to use a persistent connection
     * to return its response to the client, improving performance.
     * The content length is automatically set if the entire response fits
     * inside the response buffer.
     * <p/>
     * <p>When using HTTP 1.1 chunked encoding (which means that the response
     * has a Transfer-Encoding header), do not set the Content-Length header.
     * <p/>
     * <p>This method does not need to be either safe or idempotent.
     * Operations requested through POST can have side effects for
     * which the user can be held accountable, for example,
     * updating stored data or buying items online.
     * <p/>
     * <p>If the HTTP POST request is incorrectly formatted,
     * <code>doPost</code> returns an HTTP "Bad Request" message.
     *
     * @param req  an {@link HttpServletRequest} object that
     *             contains the request the client has made
     *             of the servlet
     * @param resp an {@link HttpServletResponse} object that
     *             contains the response the servlet sends
     *             to the client
     * @throws IOException      if an input or output error is
     *                          detected when the servlet handles
     *                          the request
     * @throws ServletException if the request for the POST
     *                          could not be handled
     * @see ServletOutputStream
     * @see ServletResponse#setContentType
     */
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        process(RequestContext.get(), METHOD_POST);
    }


    @Override
    public void destroy() {
        for (Object action : actions.values()) {
            try {
                Method method = action.getClass().getMethod("destroy");
                if (method != null) {
                    method.invoke(action);
                    log("!!!!!!!!! " + action.getClass().getSimpleName() + " destroy !!!!!!!!!");
                }
            } catch (NoSuchMethodException e) {
            } catch (Exception e) {
                log("Unabled to destroy action: " + action.getClass().getSimpleName(), e);
            }
        }
        super.destroy();
    }

    protected void process(RequestContext ctx, byte req_method) {
        try {
            /*ctx.response().setContentType("text/html;charset=utf-8");
            if (_process(ctx, req_method)) {
                if (ctx.response().getStatus()<300) {
                    ctx.response().resetBuffer();
                }
            }*/
            _process(ctx, req_method);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * @param ctx
     * @param req_method
     * @return
     * @throws IOException
     * @pre /action/zbRewardApi/reward_detail
     */
    private boolean _process(RequestContext ctx, byte req_method) throws Exception {
        String requestURI = ctx.uri();
        String[] parts = StringUtils.split(requestURI, '/');  // [action/user/change_email --> action,user,change_email]
        if (parts.length < 2) {
            ctx.not_found();
            return false;
        }
        //加载action类
        Object action = this._loadAction(parts[1]);
        if (action == null) {
            ctx.not_found();
            return false;
        }
        String action_method_name = (parts.length > 2) ? parts[2] : "index";
        Method m_action = this._getActionMethod(action, action_method_name);

        if (m_action == null) {
            ctx.not_found();
            return false;
        }
        if (req_method != METHOD_POST && m_action.isAnnotationPresent(Annotation.PostMethod.class)) {
            ctx.not_found();
            return false;
        }
        if (req_method != METHOD_GET && m_action.isAnnotationPresent(Annotation.GetMethod.class)) {
            ctx.not_found();
            return false;
        }

        if (action instanceof ActionCallback) {  //action 类回调接口,需要implements ActionCallback接口
            try {
                ((ActionCallback) action).beforeCall(m_action, ctx);
            } catch (Exception e) {
                return false;
            }
        }

        //获取参数
        int arg_count = m_action.getParameterTypes().length;
        switch (arg_count) {
            case 0:// login()
                m_action.invoke(action);
                break;
            case 1:// login(RequestContext ctx)
                m_action.invoke(action, ctx);
                break;
            case 2:// login(HttpServletRequest req, HttpServletResponse res)
                m_action.invoke(action, ctx.request(), ctx.response());
                break;
            case 3:// login(HttpServletRequest req, HttpServletResponse res, String[] extParams)
                StringBuilder params = new StringBuilder();
                for (int i = 0; i < parts.length; i++) {
                    if (StringUtils.isBlank(parts[i])) {
                        continue;
                    }
                    if (params.length() > 0) {
                        params.append('/');
                    }
                    params.append(parts[i]);
                }
                boolean isLong = m_action.getParameterTypes()[2].equals(long.class);
                m_action.invoke(action, ctx.request(), ctx.response(), isLong ? NumberUtils.toLong(params.toString(), -1L) : params.toString());
                break;
            default:
                ctx.not_found();
                return false;
        }
        if (action instanceof ActionCallback) {
            ((ActionCallback) action).afterCall(m_action, ctx);
        }
        return true;
    }

    /**
     * 加载Action类
     *
     * @param action_name
     * @return
     */
    private Object _loadAction(String action_name) {
        Object action = actions.get(action_name);
        if (action == null) {
            for (String pkg : action_packages) {
                String cls = pkg + "." + StringUtils.capitalize(action_name) + "Action";
                action = _loadActionOfFullname(action_name, cls);
                if (action == null) break;
            }
        }
        return action;
    }

    /**
     * 通过完整类名加载类
     *
     * @param action_name
     * @param cls
     * @return
     */
    private Object _loadActionOfFullname(String action_name, String cls) {
        Object action = null;
        try {
            action = Class.forName(cls).newInstance();
            try {
                Method action_init_method = action.getClass().getMethod("init", ServletContext.class);  //执行action类中的init方法
                action_init_method.invoke(action, getServletContext());
            } catch (NoSuchMethodException e) {

            } catch (InvocationTargetException e1) {
                e1.printStackTrace();
            }
            if (!actions.containsKey(action_name)) {
                actions.put(action_name, action);
            }
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e3) {
            e3.printStackTrace();
        }
        return action;
    }

    /**
     * 加载action类的方法
     *
     * @param action
     * @param method
     * @return
     */
    private Method _getActionMethod(Object action, String method) {
        String key = action.getClass().getSimpleName() + '.' + method;
        Method m = methods.get(key);
        if (m != null)
            return m;
        for (Method m1 : action.getClass().getMethods()) {
            if (m1.getModifiers() == Modifier.PUBLIC && m1.getName().equals(method)) {
                methods.put(key, m1);
                return m1;
            }
        }
        return null;
    }

    /**
     * 加载通过注解配置的action类
     */
    private void loadAnnotationMethod() {
        Set<String> classes = getProjectClassName(RequestContext.class.getResource("/").getFile(), "", true);
        for (String clsStr : classes) {
            String cls = clsStr.substring(1);//  .my.view.DebugTool -> my.view.DebugTool
            Object action = null;
            try {
                if (!Class.forName(cls).isInterface()) {
                    action = Class.forName(cls).newInstance();
                    if (action != null && action.getClass().isAnnotationPresent(Annotation.RequestMapping.class)) {
                        Annotation.RequestMapping requestMapping = action.getClass().getAnnotation(Annotation.RequestMapping.class);
                        String class_anno = requestMapping.value();
                        String method_anno = "/";
                        class_anno = class_anno.startsWith("/") ? class_anno : "/" + class_anno;

                        for (Method method : action.getClass().getMethods()) {
                            if (method.isAnnotationPresent(Annotation.RequestMapping.class) && method.getModifiers() == Modifier.PUBLIC) {
                                requestMapping = method.getDeclaredAnnotation(Annotation.RequestMapping.class);
                                method_anno = requestMapping.value().startsWith("/") ? requestMapping.value() : "/" + requestMapping.value();
                                if (!methods.containsKey(class_anno + method_anno)) {
                                    methods.put(class_anno + method_anno, method);
                                    log.info(class_anno + method_anno);
                                }
                                if (!actions.containsKey(class_anno)) {
                                    actions.put(class_anno + method_anno, action);
                                }
                            }
                        }
                    }
                }
            } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

    private Set<String> getProjectClassName(String filePath, String packageName, boolean isRecursion) {
        Set<String> className = new HashSet<>();
        File file = new File(filePath);
        File[] files = file.listFiles();
        if (files != null) {
            for (File childFile : files) {
                if (childFile.isDirectory()) {
                    if (isRecursion) {
                        className.addAll(getProjectClassName(childFile.getPath(), packageName + "." + childFile.getName(), isRecursion));
                    }
                } else {
                    String fileName = childFile.getName();
                    if (fileName.endsWith(".class") && !fileName.contains("$")) {
                        className.add(packageName + "." + fileName.replace(".class", ""));
                    }
                }
            }
        }
        return className;
    }
}
